Chuyển tới nội dung chính
Phiên bản: 8.0.0

Widget Element

Element là thành phần cơ bản nhất trong hệ thống Page Builder, đại diện cho một block nội dung có thể cấu hình và tái sử dụng. Mỗi Element được định nghĩa bởi một class PHP, cung cấp form cấu hình trong admin và render HTML trên frontend.

1. Tổng Quan Kiến Trúc

1.1 Hệ thống phân cấp Class

1.2 Ba loại Element trong Builder

LoạiVị trí trong widget.jsonSử dụng
Header Elementselements.headerChỉ dùng trong Header Builder
Footer Elementselements.footerChỉ dùng trong Footer Builder
General Elementselements.generalDùng được trong cả Header, Footer và Home/Page Builder

1.3 Element Methods

Một lớp khung xương widget đơn giản sẽ trông như sau:

<?php

use SkillDo\Cms\Element\Element;
use SkillDo\Cms\Support\Theme;

class MyElementWidget extends Element
{
function __construct() {}

public function icon(): string {}

public function category(): string {}

public function form(): void {}

public function widget(): void {}

public function default(): void {}

public function cssBuilder(): string {}
}

2. Element Data

2.1 Id và Name

Để đặt id duy nhất cho element và tên hiển thị trong admin, bạn cần gọi hàm khởi tạo của parent trong constructor:

<?php

use SkillDo\Cms\Element\Element;
use SkillDo\Cms\Support\Theme;

class MyElementWidget extends Element
{
function __construct()
{
// Tham số 1: Key duy nhất (tên class)
// Tham số 2: Tên hiển thị trong admin
parent::__construct('MyElementWidget', 'Tên Element');
}
}

2.2 Element Category

Danh mục Element được sử dụng để sắp xếp các element thành các nhóm.

<?php

use SkillDo\Cms\Element\Element;
use SkillDo\Cms\Support\Theme;

class MyElementWidget extends Element
{
/**
* Category để nhóm element trong sidebar admin
* Các category có sẵn: layout, basic, general, header,
* heading, footer, ecommerce
*/
public function category(): string
{
return 'basic';
}
}

Khi Builder được khởi tạo, nó sẽ đăng ký một số danh mục mặc định.

KeyTên hiển thịMô tả
layoutBố cụcContainer, Inner Section
basicCơ bảnHeading, Image, Text, Video, Button
generalChungElements chung
headerHeaderLogo, Cart, Search
headingTiêu đềCác kiểu heading
footerFooterFooter blocks
ecommerceThương mại điện tửProducts, Cart

2.3 Element Icon

Mỗi Element không chỉ có tên mà còn có một biểu tượng. Các biểu tượng này được hiển thị ở các vị trí khác nhau trong Trình chỉnh sửa

<?php

use SkillDo\Cms\Element\Element;
use SkillDo\Cms\Support\Theme;

class MyElementWidget extends Element
{
/**
* Icon hiển thị trong danh sách elements
* Trả về key trong danh sách icons đã đăng ký (ElementManager::getIcon)
* Các key có sẵn: heading, image, text-editor, video, button,
* icon, icon-box, divider, counter, tabs,
* accordion, form, nav-menu, google-maps, ...
*/
public function icon(): string
{
return 'icon-box';
}
}

Danh sách icon mặc định

Containercontainer
Asset 336inner-section
Asset 264heading
Asset 256image
Asset 262text-editor
Asset 253video
video-list
Asset 341button
Asset 123star-rating
Asset 334divider
Asset 257google-maps
Asset 209facebook
Asset 258icon
Asset 316image-box
Asset 314icon-box
icons-box
Asset 321basic-gallery
Asset 281image-carousel
Asset 260icon-list
Asset 331counter
Asset 274tabs
Asset 350accordion
Asset 266toggle
Asset 277social-icons
Asset 286progress-bar
Asset 261menu-anchor
Asset 321gallery
dual-image
Asset 304list
Asset 325form
Asset 309nav-menu
menu-mega-vertical
loop-carousel
Asset 220media-carousel
Asset 122reviews
Asset 170logo
Asset 222login
Asset 169search-bar
products
Asset 156product-images
Asset 161cart

3. Cấu Trúc Thư Mục

3.1 Cấu trúc chuẩn cho một Element

views/theme-store/widget/elements/{element-name}/
├── {element-name}.widget.php # Class chính (extends Element)
├── views/
│ └── view.blade.php # Template hiển thị
└── assets/ # (Tùy chọn) CSS/LESS/JS riêng
├── {element-name}.css
├── {element-name}.less
└── {element-name}.js

3.2 Ví dụ thực tế

widget/elements/video/
├── video.widget.php # VideoWidgetElement class
├── views/
│ └── view.blade.php
└── assets/
├── video-widget.less
├── video-widget.css
└── video-widget.css.map

4. Tạo Element Cơ Bản (Step-by-step)

Bước 1: Tạo file Widget Class

Tạo file views/theme-store/widget/elements/my-element/my-element.widget.php:

<?php

use SkillDo\Cms\Element\Element;
use SkillDo\Cms\Support\Theme;

class MyElementWidget extends Element
{
function __construct()
{
// Tham số 1: Key duy nhất (tên class)
// Tham số 2: Tên hiển thị trong admin
parent::__construct('MyElementWidget', 'Tên Element');
}

/**
* Icon hiển thị trong danh sách elements
* Trả về key trong danh sách icons đã đăng ký (ElementManager::getIcon)
* Các key có sẵn: heading, image, text-editor, video, button,
* icon, icon-box, divider, counter, tabs,
* accordion, form, nav-menu, google-maps, ...
*/
public function icon(): string
{
return 'icon-box';
}

/**
* Category để nhóm element trong sidebar admin
* Các category có sẵn: layout, basic, general, header,
* heading, footer, ecommerce
*/
public function category(): string
{
return 'basic';
}

/**
* Khai báo form cấu hình cho element trong admin
*/
public function form(): void
{
// Thêm fields vào tab "Nội dung" (generate)
$this->tabs('generate')->adds(function (\SkillDo\Cms\Form\Form $form)
{
$form->text('title', ['label' => 'Tiêu đề']);
$form->wysiwyg('content', ['label' => 'Nội dung']);
});

// QUAN TRỌNG: Luôn gọi parent::form() ở cuối
parent::form();
}

/**
* Render HTML đầu ra cho element
*/
public function widget(): void
{
Theme::view($this->getDir().'/views/view', [
'id' => $this->id,
'options' => $this->options,
]);
}

/**
* (Tùy chọn) Thiết lập giá trị mặc định
*/
public function default(): void
{
$this->options->title = $this->options->title ?? 'Tiêu đề mặc định';
$this->options->content = $this->options->content ?? 'Nội dung mặc định...';
}
}

Bước 2: Tạo View Template

Tạo file views/theme-store/widget/elements/my-element/views/view.blade.php:

<div class="my-element-widget">
@if(!empty($options->title))
<h3 class="title">{!! $options->title !!}</h3>
@endif

@if(!empty($options->content))
<div class="content">{!! $options->content !!}</div>
@endif
</div>

Bước 3: Đăng ký Element trong widget.json

Mở file views/theme-store/widget/widget.json và thêm vào mục elements:

{
"elements": {
"general": {
"MyElementWidget": {
"path": "widget/elements/my-element/my-element.widget.php"
}
}
}
}

[!IMPORTANT] Key trong JSON ("MyElementWidget") phải trùng khớp chính xác với tên class PHP. Hệ thống sử dụng key này để tìm và khởi tạo class.

[!NOTE]

  • Đăng ký trong elements.header → chỉ dùng trong Header Builder
  • Đăng ký trong elements.footer → chỉ dùng trong Footer Builder
  • Đăng ký trong elements.general → dùng được ở mọi builder (header, footer, home, page)

5. Hệ Thống Form — Khai Báo Fields

5.1 Cấu trúc Tab

Mỗi Element có 3 tab mặc định:

TabKeyMục đích
✏️ Nội dunggenerateDữ liệu chính (text, image, select...)
🎨 Kiểu dángstyleTùy chỉnh giao diện (màu sắc, font, border...)
⚙️ Nâng caoadvancedSpacing, Motion Effects (tự động thêm bởi Element)

5.2 Thêm fields vào tab

public function form(): void
{
// Tab "Nội dung"
$this->tabs('generate')->adds(function (\SkillDo\Cms\Form\Form $form)
{
// Các field input sẽ được thêm ở đây
});

// Tab "Kiểu dáng"
$this->tabs('style')->adds(function (\SkillDo\Cms\Form\Form $form)
{
// Các field style sẽ được thêm ở đây
});

parent::form();
}

5.3 Các loại field phổ biến

Text & Content

// Text input đơn giản
$form->text('field_name', ['label' => 'Label hiển thị']);

// Text hỗ trợ đa ngôn ngữ
$form->text('field_name', ['label' => 'Label', 'language' => true]);

// WYSIWYG Editor (TinyMCE)
$form->wysiwyg('content', ['label' => 'Nội dung']);

// Textarea
$form->textarea('description', ['label' => 'Mô tả']);

// Number input
$form->number('count', ['value' => 5, 'label' => 'Số lượng', 'start' => 6]);

Media

// Image picker
$form->image('image', ['label' => 'Chọn ảnh']);

// File upload
$form->file('document', ['label' => 'Tải file']);

Selection

// Select dropdown
$form->select('position', [
'label' => 'Vị trí',
'value' => 'left' // Giá trị mặc định
])->options([
'left' => 'Trái',
'center' => 'Giữa',
'right' => 'Phải',
]);

// Switch (bật/tắt)
$form->switch('show_title', ['label' => 'Hiển thị tiêu đề', 'value' => true]);

Style Builder Fields

// Box building (border, border-radius, box-shadow)
$form->boxBuilding('boxStyle', [
'customInput' => [
'background' => false,
'hover' => false,
]
])->popup(false);

// Text building (font-size, font-weight, color, line-height...)
$form->textBuilding('titleStyle', [
'label' => 'Style tiêu đề'
])->popup(false);

// Button building (background, padding, border, hover...)
$form->buttonBuilding('buttonStyle', [
'label' => 'Style nút bấm'
]);

// Spacing (margin, padding) — đã tự động thêm trong tab Advanced
$form->spacing('spacing', ['label' => 'Khoảng cách', 'start' => 12]);

// Scroll/Motion Effects — đã tự động thêm trong tab Advanced
$form->scrollEffects('scroll_effects', ['label' => 'Motion Effects', 'popup' => true], []);

5.4 Nhóm Fields (Group)

Sử dụng addGroup() + groupFormBox() để tạo các nhóm có thể collapse:

$this->tabs('style')->adds(function (\SkillDo\Cms\Form\Form $form)
{
// Nhóm "Ảnh" - mở sẵn (active = true)
$form->addGroup(function (\SkillDo\Cms\Form\Form $form)
{
$form->boxBuilding('imageBox', [
'customInput' => ['background' => false, 'hover' => false]
])->popup(false);

}, $this->groupFormBox('Ảnh', 'imageGroup', true));

// Nhóm "Tiêu đề" - đóng mặc định
$form->addGroup(function (\SkillDo\Cms\Form\Form $form)
{
$form->textBuilding('titleText', [
'label' => 'Style tiêu đề'
])->popup(false);

}, $this->groupFormBox('Tiêu đề', 'titleGroup'));
});

5.5 Thêm Tab tùy chỉnh

// Thêm tab mới sau tab 'generate'
$customTab = $this->addTab(
'custom_tab', // ID
'Tab tùy chỉnh', // Tên hiển thị
'<i class="fa-thin fa-star"></i>', // Icon
['after' => 'generate'] // Vị trí (sau tab nào)
);

// Thêm fields vào tab mới
$customTab->adds(function (\SkillDo\Cms\Form\Form $form) {
$form->text('custom_field', ['label' => 'Field tùy chỉnh']);
});

6. CSS Builder — Tùy Chỉnh Giao Diện (cssSelector)

6.1 Tổng quan

Sử dụng cssSelector() để tạo CSS động dựa trên dữ liệu từ các style builder fields. Method này tự động xử lý responsive (desktop/tablet/mobile) và hover/active states — không cần khai báo device mapping thủ công.

[!WARNING] Không sử dụng cssStyle() — đây là API cũ, yêu cầu khai báo options => ['desktop' => 'css', 'tablet' => 'cssTablet', ...] thủ công. Hãy dùng cssSelector() cho tất cả element mới.

6.2 Cú pháp cssSelector

$this->cssSelector($selectors, ...$properties)

Tham số:

  • $selectors — CSS selector (string hoặc array)
  • ...$properties — Một hoặc nhiều mảng ['data' => ..., 'style' => ...]

6.3 Các cách truyền Selectors

Cách 1: String selector (đơn giản nhất)

Hệ thống tự sinh normal + hover (thêm :hover) + active (thêm .active):

// Tự động áp dụng cho:
// normal: .item
// hover: .item:hover
// active: .item.active
$this->cssSelector('.item', [
'data' => $this->options->boxStyle ?? [],
'style' => 'box',
]);

Cách 2: Array selector (tùy chỉnh hover target)

Dùng khi hover selector khác với normal selector:

// normal: .item .title .header
// hover: .item:hover .title .header (tự thêm :hover vào hover selector)
$this->cssSelector([
'normal' => '.item .title .header',
'hover' => '.item:hover .title .header',
], [
'data' => $this->options->titleStyle ?? [],
'style' => 'text',
]);

[!TIP] Pattern phổ biến: hover ở phần tử cha (.item:hover) nhưng style thay đổi ở phần tử con (.title .header). Đây là lý do cần array selector.

Cách 3: Nhiều properties trên cùng selector

Truyền nhiều ['data' => ..., 'style' => ...] dưới dạng variadic arguments:

$this->cssSelector('.tab-button',
[
'data' => $this->options->tabButtonBg ?? [],
'style' => 'background',
],
[
'data' => $this->options->tabButtonBorder ?? [],
'style' => 'border',
],
[
'data' => $this->options->tabButtonShadow ?? [],
'style' => 'boxShadow',
],
[
'data' => $this->options->tabButtonTxt ?? [],
'style' => 'text',
],
[
'data' => $this->options->tabSpacing ?? [],
'style' => 'spacing',
]
);

6.4 Các Style Type

Style TypeMô tảDùng với field
textFont-size, font-weight, color, text-align, line-height, letter-spacingtextBuilding()
boxBorder, border-radius, box-shadow, background, paddingboxBuilding()
backgroundBackground-color, background-image, gradientbackground()
borderBorder-width, border-style, border-color, border-radiusborder()
boxShadowBox-shadowboxShadow()
spacingMargin, paddingspacing()
colorColor propertycolor()

[!NOTE] Bạn có thể viết 'style' => 'text' hoặc 'style' => 'cssText' — hệ thống tự động thêm prefix css + ucfirst nếu chưa có.

6.5 Ví dụ đầy đủ cssBuilder()

public function cssBuilder(): string
{
// 1. CSS Variables
$this->cssVariables('--img-ration', '56.25%');
$this->cssVariables('--item-number', $this->options->desktopNumberShow);

// 2. Box style cho container
$this->cssSelector('.item', [
'data' => $this->options->boxStyle ?? [],
'style' => 'box',
]);

// 3. Border cho ảnh
$this->cssSelector('.item .img', [
'data' => $this->options->imageBorder ?? [],
'style' => 'border',
]);

// 4. Text style với hover (hover ở parent, style ở child)
$this->cssSelector([
'normal' => '.item .title .header',
'hover' => '.item:hover .title .header',
], [
'data' => $this->options->titleStyle ?? [],
'style' => 'text',
]);

// 5. Nhiều properties cùng lúc
$this->cssSelector('.wrapper',
[
'data' => $this->options->wrapperBg ?? [],
'style' => 'background',
],
[
'data' => $this->options->wrapperBorder ?? [],
'style' => 'border',
]
);

// QUAN TRỌNG: Luôn return cssBuild() ở cuối
return $this->cssBuild();
}

6.6 So sánh cssStyle (cũ) vs cssSelector (mới)

cssStyle ❌ (cũ)cssSelector ✅ (mới)
Device mappingPhải khai báo thủ công: 'options' => ['desktop' => 'css', 'tablet' => 'cssTablet', 'mobile' => 'cssMobile']Tự động xử lý 3 devices
HoverPhải khai báo 'hover' => 'cssHover' hoặc tạo selector riêngTự động từ selectors array
Active stateKhông hỗ trợ trực tiếpTự động sinh .active selector
Nhiều propertiesGọi nhiều lần cssStyle()Truyền variadic args
Style name'style' => 'cssText''style' => 'text' (hoặc 'cssText')

6.7 Sử dụng CSS/LESS Assets

function __construct()
{
parent::__construct('MyElementWidget', 'Tên Element');

// Đăng ký LESS file (tự compile sang CSS)
$this->assets('assets/my-element.less');

// Hoặc CSS file
$this->assets('assets/my-element.css');

// Hoặc JS file
$this->assets('assets/my-element.js');
}

[!TIP] Đường dẫn assets là tương đối so với thư mục widget element. Ví dụ nếu widget ở widget/elements/my-element/ thì assets/style.less sẽ trỏ tới widget/elements/my-element/assets/style.less.


7. JavaScript cho Element

7.1 Tổng quan

JS của element phải được đăng ký vào hệ thống thông qua sự kiện elementor/frontend/init. Hệ thống fire sự kiện này sau khi trang load xong, trước khi khởi tạo các widget.

Luồng xử lý:

7.2 Đăng ký JS File

Khai báo JS file trong constructor của class PHP — giống như đăng ký CSS/LESS:

function __construct()
{
parent::__construct('MyElementWidget', 'Tên Element');

// Đăng ký JS file
$this->assets('assets/my-element.js');

// Có thể kết hợp với LESS/CSS
$this->assets('assets/my-element.less');
}

7.3 Cấu trúc file JS

JS của element được viết theo dạng class ES6. Mỗi element có một class riêng với constructor để khởi tạo và destroy() để dọn dẹp.

// File: assets/my-element.js

class MyElementWidget
{
constructor(scope, $)
{
this.scope = scope; // jQuery object của .elementor-widget wrapper
this.$ = $; // jQuery instance

this.$widget = scope.find('.my-element-widget');

if (!this.$widget.length) return;

this.init();
}

init()
{
// Khởi tạo logic JS cho widget
this.bindEvents();
}

bindEvents()
{
// Bind event listeners tại đây
}

destroy()
{
// Dọn dẹp: destroy plugins, remove event listeners, clear timers...
this.scope = null;
}
}

$(window).on('elementor/frontend/init', function ()
{
elementorFrontend.hooks.addAction(
'frontend/ready/MyElementWidget.default', // hook name: frontend/ready/{ClassName}.default
function (scope, $)
{
const instance = new MyElementWidget(scope, $);

scope.data('onDestroy', function () {
instance.destroy();
});
}
);
});

Tên hook: frontend/ready/{WidgetClassName}.default

  • WidgetClassName phải trùng khớp chính xác với tên class PHP
  • Suffix .default là cố định

7.4 Ví dụ: Element với Swiper Slider

// File: assets/product-slider.js

class ProductSliderWidgetElement
{
constructor(scope, $)
{
this.scope = scope;
this.swiper = null;

const $container = scope.find('.swiper-container');

if (!$container.length) return;

this.swiper = new Swiper($container[0], {
slidesPerView: 3,
spaceBetween: 20,
loop: true,
navigation: {
nextEl: scope.find('.swiper-button-next')[0],
prevEl: scope.find('.swiper-button-prev')[0],
},
breakpoints: {
0: { slidesPerView: 1 },
768: { slidesPerView: 2 },
992: { slidesPerView: 3 },
}
});
}

destroy()
{
if (this.swiper)
{
this.swiper.destroy(true, true);
this.swiper = null;
}
}
}

$(window).on('elementor/frontend/init', function ()
{
elementorFrontend.hooks.addAction(
'frontend/ready/ProductSliderWidgetElement.default',
function (scope, $)
{
const instance = new ProductSliderWidgetElement(scope, $);

scope.data('onDestroy', function () {
instance.destroy();
});
}
);
});

7.5 Ví dụ: Element với Tab

// File: assets/tabs-widget.js

class TabsWidgetElement
{
constructor(scope, $)
{
this.scope = scope;

this.$tabs = scope.find('.tab-nav-item');
this.$contents = scope.find('.tab-content-item');

if (!this.$tabs.length) return;

this.bindEvents();
this.activateTab(0);
}

activateTab(index)
{
this.$tabs.removeClass('active').eq(index).addClass('active');
this.$contents.removeClass('active').eq(index).addClass('active');
}

bindEvents()
{
const self = this;

this.$tabs.on('click.tabsWidget', function ()
{
self.activateTab($(this).index());
return false;
});
}

destroy()
{
this.$tabs.off('click.tabsWidget');
}
}

$(window).on('elementor/frontend/init', function ()
{
elementorFrontend.hooks.addAction(
'frontend/ready/TabsWidgetElement.default',
function (scope, $)
{
const instance = new TabsWidgetElement(scope, $);

scope.data('onDestroy', function () {
instance.destroy();
});
}
);
});

7.6 Lưu ý quan trọng

[!WARNING] Không đặt code khởi tạo ngoài elementor/frontend/init. Nếu đặt trong $(document).ready(), widget sẽ không được khởi tạo lại khi re-render trong Builder.

[!IMPORTANT] Luôn implement destroy() và đăng ký scope.data('onDestroy', ...). Method destroy() được gọi tự động bởi hệ thống khi widget bị xóa hoặc cần re-render. Phải dọn dẹp toàn bộ: hủy plugin (.destroy()), gỡ event listener (.off()), clear timer/observer.

[!TIP] Namespace event listener khi dùng jQuery (ví dụ: 'click.myWidget') để off() trong destroy() mà không ảnh hưởng tới các listener khác cùng event trên element đó.


8. Ví Dụ Đầy Đủ — Element "Alert Box"

8.1 Cấu trúc thư mục

widget/elements/alert-box/
├── alert-box.widget.php
├── views/
│ └── view.blade.php
└── assets/
├── alert-box.less
└── alert-box.js

8.2 File Widget Class

<?php
// File: views/theme-store/widget/elements/alert-box/alert-box.widget.php

use SkillDo\Cms\Element\Element;
use SkillDo\Cms\Support\Theme;

class AlertBoxWidgetElement extends Element
{
function __construct()
{
parent::__construct('AlertBoxWidgetElement', 'Alert Box');
$this->assets('assets/alert-box.less');
$this->assets('assets/alert-box.js');
}

public function icon(): string
{
return 'toggle'; // Sử dụng icon có sẵn
}

public function category(): string
{
return 'basic';
}

public function form(): void
{
// ========== TAB NỘI DUNG ==========
$this->tabs('generate')->adds(function (\SkillDo\Cms\Form\Form $form)
{
$form->select('type', [
'label' => 'Loại thông báo',
'value' => 'info'
])->options([
'info' => 'Thông tin',
'success' => 'Thành công',
'warning' => 'Cảnh báo',
'danger' => 'Lỗi',
]);

$form->text('title', [
'label' => 'Tiêu đề',
'language' => true
]);

$form->wysiwyg('content', ['label' => 'Nội dung']);

$form->image('icon_image', ['label' => 'Icon tùy chỉnh (tùy chọn)']);

$form->switch('dismissible', [
'label' => 'Cho phép đóng',
'value' => false
]);
});

// ========== TAB KIỂU DÁNG ==========
$this->tabs('style')->adds(function (\SkillDo\Cms\Form\Form $form)
{
$form->addGroup(function (\SkillDo\Cms\Form\Form $form)
{
$form->textBuilding('titleStyle', [
'label' => 'Style tiêu đề'
])->popup(false);
}, $this->groupFormBox('Tiêu đề', 'titleStyleGroup', true));

$form->addGroup(function (\SkillDo\Cms\Form\Form $form)
{
$form->textBuilding('contentStyle', [
'label' => 'Style nội dung'
])->popup(false);
}, $this->groupFormBox('Nội dung', 'contentStyleGroup'));

$form->addGroup(function (\SkillDo\Cms\Form\Form $form)
{
$form->boxBuilding('containerBox', [
'customInput' => [
'background' => true,
'hover' => false,
]
])->popup(false);
}, $this->groupFormBox('Container', 'containerGroup'));
});

parent::form();
}

public function widget(): void
{
Theme::view($this->getDir().'/views/view', [
'id' => $this->id,
'options' => $this->options,
]);
}

public function cssBuilder(): string
{
// CSS cho tiêu đề (text style)
$this->cssSelector('.alert-title', [
'data' => $this->options->titleStyle ?? [],
'style' => 'text',
]);

// CSS cho nội dung (text style)
$this->cssSelector('.alert-content', [
'data' => $this->options->contentStyle ?? [],
'style' => 'text',
]);

// CSS cho container (box + background cùng lúc)
$this->cssSelector('.alert-box',
[
'data' => $this->options->containerBox ?? [],
'style' => 'box',
]
);

return $this->cssBuild();
}

public function default(): void
{
$this->options->type = $this->options->type ?? 'info';
$this->options->title = $this->options->title ?? 'Tiêu đề thông báo';
$this->options->content = $this->options->content ?? 'Đây là nội dung thông báo mẫu.';
$this->options->dismissible = $this->options->dismissible ?? false;
}
}

8.3 File Blade View

{{-- File: views/theme-store/widget/elements/alert-box/views/view.blade.php --}}

@php
$typeClass = match($options->type ?? 'info') {
'success' => 'alert-success',
'warning' => 'alert-warning',
'danger' => 'alert-danger',
default => 'alert-info',
};
@endphp

<div class="alert-box {{ $typeClass }}" role="alert">
@if(!empty($options->icon_image))
<div class="alert-icon">
<img src="{{ Url::image($options->icon_image) }}" alt="{{ $options->title ?? '' }}">
</div>
@endif

<div class="alert-body">
@if(!empty($options->title))
<div class="alert-title">{!! $options->title !!}</div>
@endif

@if(!empty($options->content))
<div class="alert-content">{!! $options->content !!}</div>
@endif
</div>

@if(!empty($options->dismissible))
<button type="button" class="alert-close" aria-label="Close">
<i class="fa-solid fa-xmark"></i>
</button>
@endif
</div>

8.4 File LESS

// File: views/theme-store/widget/elements/alert-box/assets/alert-box.less

.alert-box {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px 20px;
border-radius: 8px;
position: relative;

&.alert-info {
background: #e8f4fd;
border-left: 4px solid #2196f3;
}
&.alert-success {
background: #e8f5e9;
border-left: 4px solid #4caf50;
}
&.alert-warning {
background: #fff8e1;
border-left: 4px solid #ff9800;
}
&.alert-danger {
background: #fde8e8;
border-left: 4px solid #f44336;
}

.alert-icon {
flex-shrink: 0;
width: 32px;
height: 32px;

img {
width: 100%;
height: 100%;
object-fit: contain;
}
}

.alert-body {
flex: 1;
}

.alert-title {
font-weight: 600;
font-size: 16px;
margin-bottom: 4px;
}

.alert-content {
font-size: 14px;
line-height: 1.5;
}

.alert-close {
background: none;
border: none;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.2s;

&:hover {
opacity: 1;
}
}
}

8.5 Đăng ký trong widget.json

{
"elements": {
"general": {
"AlertBoxWidgetElement": {
"path": "widget/elements/alert-box/alert-box.widget.php"
}
}
}
}

8.6 File JavaScript

// File: views/theme-store/widget/elements/alert-box/assets/alert-box.js

class AlertBoxWidgetElement
{
constructor(scope, $)
{
this.scope = scope;
this.$closeBtn = scope.find('.alert-close');

if (!this.$closeBtn.length) return;

this.bindEvents();
}

bindEvents()
{
const self = this;

this.$closeBtn.on('click.alertBox', function ()
{
self.scope.fadeOut(300);
});
}

destroy()
{
this.$closeBtn.off('click.alertBox');
this.scope = null;
}
}

$(window).on('elementor/frontend/init', function ()
{
elementorFrontend.hooks.addAction(
'frontend/ready/AlertBoxWidgetElement.default',
function (scope, $)
{
const instance = new AlertBoxWidgetElement(scope, $);

scope.data('onDestroy', function () {
instance.destroy();
});
}
);
});

9. Tính Năng Nâng Cao

9.1 Element với AJAX callback

Dùng cho elements cần load dữ liệu động (ví dụ: danh sách sản phẩm):

// widget.json
{
"elements": {
"general": {
"ProductsWidgetElementStyle1": {
"path": "widget/elements/products/style1/products-style1.widget.php",
"ajax": {
"client": "ProductsWidgetElementStyle1::loadProduct"
}
}
}
}
}

9.2 Element chứa Widget con (Widget Container)

Element có thể chứa các widget con bên trong bằng widgetContainer():

public function widget(): void
{
$html = '';

// Render các widget con từ options->container
if (!empty($this->options->container))
{
$html = $this->widgetContainer($this->options->container);
}

echo '<div class="my-container">' . $html . '</div>';
}

9.3 Filter Hooks

Hệ thống cung cấp hooks trước khi lưu widget settings:

// Hook chung cho tất cả widgets
apply_filters('before_widget_save', $settings);

// Hook riêng cho widget cụ thể
apply_filters('wg_before_MyElementWidget_save', $settings);

9.4 Element với Scroll Effects

Tab Advanced đã tự động thêm spacingscrollEffects. Hệ thống tự động:

  1. Build data-scroll-effects attribute trên wrapper div
  2. Xử lý CSS spacing responsive (desktop/tablet/mobile)

Các loại scroll effects hỗ trợ:

  • vertical — Di chuyển dọc
  • horizontal — Di chuyển ngang
  • rotate — Xoay
  • scale — Phóng to/thu nhỏ
  • opacity — Mờ dần
  • blur — Làm mờ

10. Luồng Xử Lý (Request Flow)


11. Checklist Tạo Element Mới

  • Tạo thư mục theo cấu trúc: widget/elements/{name}/
  • Tạo class extends Element, implement 4 methods bắt buộc:
    • icon() — trả về icon key
    • category() — trả về category key
    • form() — khai báo form fields + gọi parent::form()
    • widget() — render HTML output
  • Tạo Blade view trong views/view.blade.php
  • (Tùy chọn) Tạo CSS/LESS trong assets/ + đăng ký qua $this->assets()
  • (Tùy chọn) Tạo JS trong assets/ theo class pattern + đăng ký qua $this->assets(), lắng nghe elementor/frontend/init và dùng elementorFrontend.hooks.addAction('frontend/ready/{ClassName}.default', ...)
  • (Tùy chọn) Override default() cho giá trị mặc định
  • (Tùy chọn) Override cssBuilder() cho CSS động
  • Đăng ký trong widget.json với key = tên class
  • Kiểm tra trong Admin → Builder

[!WARNING] Key trong widget.json phải giống hệt tên class PHP. Nếu class là AlertBoxWidgetElement thì key phải là "AlertBoxWidgetElement", không phải "alert_box_widget_element" hay bất kỳ biến thể nào khác.

[!CAUTION] Luôn gọi parent::form()cuối method form(). Nếu không, các field mặc định trong tab Advanced (spacing, scroll effects) sẽ không hoạt động đúng.